import unittest
import numpy as np

class ErrorVal(object):
    def __init__(self, value, error):
        self.value = value
        self.error = error

    def droperr(self):
        self.error *= 0
        return self

    def __pow__(self, power):
        return ErrorVal(self.value**power, power*self.error*self.value**(power-1))

    def __add__(self, other):
        if isinstance(other, ErrorVal):
            return ErrorVal(self.value + other.value, (self.error**2 + other.error**2)**0.5)
        else:
            return ErrorVal(self.value + other, self.error)

    def __sub__(self, other):
        if isinstance(other, ErrorVal):
            return ErrorVal(self.value - other.value, (self.error**2.0 + other.error**2.0)**0.5)
        else:
            return ErrorVal(self.value - other, self.error)

    def __mul__(self, other):
        if isinstance(other, ErrorVal):
            return ErrorVal(self.value * other.value, ((self.error*other.value)**2.0 +
                                                                              (other.error*self.value)**2.0)**0.5)
        else:
            return ErrorVal(self.value * other, self.error * other)

    def __div__(self, other):
        if isinstance(other, ErrorVal):
            return ErrorVal(self.value / other.value, (self.value/other.value)*((self.error/self.value)**2.0 +
                                                                                (other.error/other.value)**2.0)**0.5)
        else:
            return ErrorVal(self.value / other, self.error / other)

    def __radd__(self, other):
        return self.__add__(other)

    def __rsub__(self, other):
        if not isinstance(other, ErrorVal):  # this happens because of float - ErrorVal, e.g.
            return ErrorVal(other, 0.0).__sub__(self)
        else:  # should never hit this condition, I think
            return other.__sub__(self)

    def __rmul__(self, other):
        return self.__mul__(other)

    def __rdiv__(self, other):
        if not isinstance(other, ErrorVal):  # this happens because of float / ErrorVal, e.g.
            return ErrorVal(float(other), 0.0).__div__(self)
        else:  # should never hit this condition, I think
            return self.__div__(other)

    def tuple(self):
        return self.value, self.error

    def __repr__(self):
        return str(self.tuple())

    def __getitem__(self, item):
        return self.__class__(self.value[item], self.error[item])

    def pack(self):
        def packer(a, b):
            return ErrorVal(a,b)
        packer = np.vectorize(packer)
        return packer(self.value, self.error)

class TestReal(unittest.TestCase):
    def test_ops(self):
        x = ErrorVal(2., 1.)
        y = ErrorVal(1., 1.)
        np.testing.assert_almost_equal((x+y).tuple(), (3, 2**0.5))
        np.testing.assert_almost_equal((x-y).tuple(), (1, 2**0.5))
        np.testing.assert_almost_equal((x*y).tuple(), (2, 2*(1.25**0.5)))
        np.testing.assert_almost_equal((x/y).tuple(), (2, 2*(1.25**0.5)))
        np.testing.assert_almost_equal((x+3).tuple(), (5, 1))
        np.testing.assert_almost_equal((x-3).tuple(), (-1, 1))
        np.testing.assert_almost_equal((x*3).tuple(), (6, 3))
        np.testing.assert_almost_equal((x/3).tuple(), (2/3.0, 1/3.0))
        np.testing.assert_almost_equal((3+x).tuple(), (5, 1))
        np.testing.assert_almost_equal((3-x).tuple(), (1, 1))
        np.testing.assert_almost_equal((3*x).tuple(), (6, 3))
        np.testing.assert_almost_equal((3/x).tuple(), (1.5, 0.75))

if __name__ == '__main__':
    unittest.main()

